JAX - RS / Jersey jak dostosować obsługę błędów?

Uczę się JAX-RS (aka, JSR-311) używając Jersey. Udało mi się stworzyć zasób główny i bawię się parametrami:

@Path("/hello")
public class HelloWorldResource {

    @GET
    @Produces("text/html")
    public String get(
        @QueryParam("name") String name,
        @QueryParam("birthDate") Date birthDate) {

         // Return a greeting with the name and age
    }
}

To działa świetnie i obsługuje dowolny format w bieżącej lokalizacji, który jest rozumiany przez konstruktor date (String) (jak RRRR/mm/dd i mm/dd/RRRR). Ale jeśli podam wartość, która jest nieprawidłowa lub nie zrozumiała, otrzymuję odpowiedź 404.

Na przykład:

GET /hello?name=Mark&birthDate=X

404 Not Found

Jak mogę dostosować to zachowanie? Może inny kod odpowiedzi (prawdopodobnie "400 Bad Request")? A co z rejestrowaniem błędu? Może dodać opis problemu ("zły format daty") w niestandardowym nagłówku, aby pomóc w rozwiązywaniu problemów? Lub zwrócić całą odpowiedź na błąd ze szczegółami, wraz z kodem stanu 5xx?

Author: Paul Bellora, 2009-02-24

11 answers

Istnieje kilka metod dostosowywania zachowania obsługi błędów za pomocą JAX-RS. Oto trzy łatwiejsze sposoby.

Pierwszym podejściem jest utworzenie klasy wyjątku, która rozszerza Webaplicationexception.

Przykład:

public class NotAuthorizedException extends WebApplicationException {
     public NotAuthorizedException(String message) {
         super(Response.status(Response.Status.UNAUTHORIZED)
             .entity(message).type(MediaType.TEXT_PLAIN).build());
     }
}

I aby wrzucić ten nowo utworzony wyjątek wystarczy:

@Path("accounts/{accountId}/")
    public Item getItem(@PathParam("accountId") String accountId) {
       // An unauthorized user tries to enter
       throw new NotAuthorizedException("You Don't Have Permission");
}

Uwaga, nie musisz deklarować wyjątku w klauzuli throws, ponieważ Webaplicationexception jest wyjątkiem uruchomieniowym. To zwróci odpowiedź 401 na klient.

Drugim i łatwiejszym podejściem jest po prostu skonstruowanie instancji WebApplicationException bezpośrednio w kodzie. To podejście działa tak długo, jak nie trzeba implementować własnych WYJĄTKÓW aplikacji.

Przykład:

@Path("accounts/{accountId}/")
public Item getItem(@PathParam("accountId") String accountId) {
   // An unauthorized user tries to enter
   throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}

Ten kod również zwraca klientowi 401.

Oczywiście, to tylko prosty przykład. Możesz uczynić wyjątek znacznie bardziej skomplikowanym, jeśli to konieczne, i możesz wygenerować kod odpowiedzi http, którego potrzebujesz.

One other podejście polega na owinięciu istniejącego wyjątku, być może ObjectNotFoundException małą klasą owijającą, która implementuje interfejs ExceptionMapper z adnotacją @Provider. To mówi runtime JAX-RS, że jeśli wywołany zostanie zawinięty wyjątek, zwróci kod odpowiedzi zdefiniowany w ExceptionMapper.

 275
Author: Steven Levine,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-21 08:57:31
@Provider
public class BadURIExceptionMapper implements ExceptionMapper<NotFoundException> {

public Response toResponse(NotFoundException exception){

    return Response.status(Response.Status.NOT_FOUND).
    entity(new ErrorResponse(exception.getClass().toString(),
                exception.getMessage()) ).
    build();
}
}

Create above class. To obsłuży 404 (NotFoundException) i tutaj w metodzie toResponse możesz podać swoją niestandardową odpowiedź. Podobnie są ParamException itp. które trzeba zmapować, aby zapewnić dostosowane odpowiedzi.

 70
Author: Arnav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-27 16:41:56

Jersey rzuca kom.słońce.jersey.api.ParamException, gdy nie usunie wszystkich parametrów, więc jednym z rozwiązań jest utworzenie ExceptionMapper, który obsługuje tego typu wyjątki:

@Provider
public class ParamExceptionMapper implements ExceptionMapper<ParamException> {
    @Override
    public Response toResponse(ParamException exception) {
        return Response.status(Status.BAD_REQUEST).entity(exception.getParameterName() + " incorrect type").build();
    }
}
 38
Author: Jan Kronquist,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-16 00:07:54

Możesz również napisać klasę wielokrotnego użytku dla zmiennych z adnotacją QueryParam

public class DateParam {
  private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

  private Calendar date;

  public DateParam(String in) throws WebApplicationException {
    try {
      date = Calendar.getInstance();
      date.setTime(format.parse(in));
    }
    catch (ParseException exception) {
      throw new WebApplicationException(400);
    }
  }
  public Calendar getDate() {
    return date;
  }
  public String format() {
    return format.format(value.getTime());
  }
}

Następnie użyj go w ten sposób:

private @QueryParam("from") DateParam startDateParam;
private @QueryParam("to") DateParam endDateParam;
// ...
startDateParam.getDate();

Chociaż obsługa błędów jest trywialna w tym przypadku( rzucanie odpowiedzi 400), użycie tej klasy pozwala na ogólną obsługę parametrów factor-out, która może obejmować logowanie itp.

 27
Author: Charlie Brooking,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-16 12:30:23

Jedno oczywiste rozwiązanie: weź Łańcuch, przekonwertuj się na randkę. W ten sposób możesz zdefiniować żądany format, wyłapać wyjątki i ponownie wyrzucić lub dostosować wysyłany błąd. Do parsowania, SimpleDateFormat powinien działać dobrze.

Jestem pewien, że istnieją sposoby na hookowanie obsługi typów danych, ale być może wystarczy trochę prostego kodu w tym przypadku.

 11
Author: StaxMan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-28 01:01:49

Ja też lubię StaxMan prawdopodobnie zaimplementowałby to QueryParam jako ciąg znaków, a następnie zajął się konwersją, w razie potrzeby przeredagowując.

Jeśli zachowanie specyficzne dla locale jest pożądanym i oczekiwanym zachowaniem, należy użyć następującego polecenia, aby zwrócić błąd 400 BAD REQUEST:

throw new WebApplicationException(Response.Status.BAD_REQUEST);

Aby uzyskać więcej opcji, Zobacz JavaDoc dla javax.ws.RS.core.Response.Status.
 7
Author: dshaw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:26

@ QueryParam documentation says

" Typ T przypisanego parametru, pola lub właściwości musi albo:

1) być prymitywnym typem
2) mieć konstruktor, który przyjmuje pojedynczy Argument String
3) posiadać statyczną metodę o nazwie valueOf lub fromString który akceptuje pojedynczy argument Łańcuchowy (patrz np., Liczba całkowita.valueOf (String))
4) posiadają zarejestrowaną realizację javax.ws.RS.ext.ParamConverterProvider JAX-RS extension SPI that zwraca instancję javax. ws. RS. ext. ParamConverter o wartości " from string " konwersja dla typu.
5) BYĆ wykazem, zestawem lub SortedSet, gdzie T spełnia 2, 3 lub 4 powyżej. W wyniku kolekcja jest tylko do odczytu. "

Jeśli chcesz kontrolować, jaka odpowiedź trafia do użytkownika, gdy parametr zapytania w postaci ciągu znaków nie może być przekonwertowany na Twój typ T, możesz rzucić Webaplicationexception. Dropwizard zawiera następujące * klasy Param, których możesz użyć do potrzeb.

BooleanParam, DateTimeParam, IntParam, LongParam, LocalDateParam, NonEmptyStringParam, UUIDParam. Zobacz też https://github.com/dropwizard/dropwizard/tree/master/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params

Jeśli potrzebujesz Joda DateTime, po prostu użyj Dropwizard DateTimeParam.

Jeśli powyższa lista nie odpowiada twoim potrzebom, zdefiniuj własną przez rozszerzenie AbstractParam. Override parse metoda. Jeśli potrzebujesz kontroli nad ciałem odpowiedzi na błędy, nadpisanie metody błędu.

Dobry artykuł z Coda Hale na ten temat jest na http://codahale.com/what-makes-jersey-interesting-parameter-classes/

import io.dropwizard.jersey.params.AbstractParam;

import java.util.Date;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class DateParam extends AbstractParam<Date> {

    public DateParam(String input) {
        super(input);
    }

    @Override
    protected Date parse(String input) throws Exception {
        return new Date(input);
    }

    @Override
    protected Response error(String input, Exception e) {
        // customize response body if you like here by specifying entity
        return Response.status(Status.BAD_REQUEST).build();
    }
}

Konstruktor date(String arg) jest przestarzały. Użyłbym Java 8 date classes jeśli jesteś na Java 8. W przeciwnym razie zalecany jest czas Joda date.

 4
Author: Srikanth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-30 06:41:45

To jest właściwe zachowanie. Jersey spróbuje znaleźć obsługę dla Twojego wejścia i spróbuje skonstruować obiekt z podanego wejścia. W tym przypadku spróbuje wytworzyć nowy obiekt Date o wartości x dostarczonej konstruktorowi. Ponieważ jest to nieprawidłowa data, zgodnie z konwencją Jersey zwróci 404.

To co możesz zrobić to przepisać i umieścić datę urodzenia jako ciąg znaków, a następnie spróbować parsować, a jeśli nie dostaniesz tego, co chcesz, możesz rzucić dowolny wyjątek, który chcesz przez każdy z mechanizmów mapowania wyjątków (jest ich kilka).

 1
Author: ACV,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-20 20:08:27

Miałem do czynienia z tym samym problemem.

Chciałem wyłapać wszystkie błędy w centralnym miejscu i przekształcić je.

Poniżej znajduje się kod, jak sobie z tym poradziłem.

Utwórz następującą klasę, która implementuje ExceptionMapper i dodaj @Provider adnotację do tej klasy. To zajmie się wszystkimi wyjątkami.

Nadpisuje metodę toResponse i zwraca obiekt odpowiedzi wypełniony niestandardowymi danymi.

//ExceptionMapperProvider.java
/**
 * exception thrown by restful endpoints will be caught and transformed here
 * so that client gets a proper error message
 */
@Provider
public class ExceptionMapperProvider implements ExceptionMapper<Throwable> {
    private final ErrorTransformer errorTransformer = new ErrorTransformer();

    public ExceptionMapperProvider() {

    }

    @Override
    public Response toResponse(Throwable throwable) {
        //transforming the error using the custom logic of ErrorTransformer 
        final ServiceError errorResponse = errorTransformer.getErrorResponse(throwable);
        final ResponseBuilder responseBuilder = Response.status(errorResponse.getStatus());

        if (errorResponse.getBody().isPresent()) {
            responseBuilder.type(MediaType.APPLICATION_JSON_TYPE);
            responseBuilder.entity(errorResponse.getBody().get());
        }

        for (Map.Entry<String, String> header : errorResponse.getHeaders().entrySet()) {
            responseBuilder.header(header.getKey(), header.getValue());
        }

        return responseBuilder.build();
    }
}

// ErrorTransformer.java
/**
 * Error transformation logic
 */
public class ErrorTransformer {
    public ServiceError getErrorResponse(Throwable throwable) {
        ServiceError serviceError = new ServiceError();
        //add you logic here
        serviceError.setStatus(getStatus(throwable));
        serviceError.setBody(getBody(throwable));
        serviceError.setHeaders(getHeaders(throwable));

    }
    private String getStatus(Throwable throwable) {
        //your logic
    }
    private Optional<String> getBody(Throwable throwable) {
        //your logic
    }
    private Map<String, String> getHeaders(Throwable throwable) {
        //your logic
    }
}

//ServiceError.java
/**
 * error data holder
 */
public class ServiceError {
    private int status;
    private Map<String, String> headers;
    private Optional<String> body;
    //setters and getters
}
 1
Author: suraj.tripathi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-08 12:06:33

Podejście 1: poprzez rozszerzenie klasy WebApplicationException

Utwórz nowy wyjątek rozszerzając Webaplicationexception

public class RestException extends WebApplicationException {

         private static final long serialVersionUID = 1L;

         public RestException(String message, Status status) {
         super(Response.status(status).entity(message).type(MediaType.TEXT_PLAIN).build());
         }
}

Teraz rzucaj 'RestException' w razie potrzeby.

public static Employee getEmployee(int id) {

         Employee emp = employees.get(id);

         if (emp == null) {
                 throw new RestException("Employee with id " + id + " not exist", Status.NOT_FOUND);
         }
         return emp;
}

Możesz zobaczyć pełną aplikację pod tym link .

Podejście 2: Implementacja ExceptionMapper

Następujący po maperze wyjątek od typu "DataNotFoundException"

@Provider
public class DataNotFoundExceptionMapper implements
        ExceptionMapper<DataNotFoundException> {

    @Override
    public Response toResponse(DataNotFoundException ex) {
        ErrorMessage model = new ErrorMessage(ex.getErrorCode(),
                ex.getMessage());
        return Response.status(Status.NOT_FOUND).entity(model).build();
    }

}

Możesz zobaczyć pełną aplikację pod tym link .

 1
Author: Hari Krishna,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-01-10 10:25:31

Tak jak rozszerzenie @ Steven Lavine answer w przypadku, gdy chcesz otworzyć okno logowania do przeglądarki. Trudno mi było poprawnie zwrócić odpowiedź (MDN HTTP Authentication ) z filtra w przypadku, gdy użytkownik nie był jeszcze uwierzytelniony

To pomogło mi zbudować odpowiedź, aby wymusić logowanie do przeglądarki, zwróć uwagę na dodatkową modyfikację nagłówków. Ustawi to kod stanu na 401 i ustaw nagłówek, który spowoduje, że przeglądarka otworzy nazwę użytkownika/hasło dialog.

// The extended Exception class
public class NotLoggedInException extends WebApplicationException {
  public NotLoggedInException(String message) {
    super(Response.status(Response.Status.UNAUTHORIZED)
      .entity(message)
      .type(MediaType.TEXT_PLAIN)
      .header("WWW-Authenticate", "Basic realm=SecuredApp").build()); 
  }
}

// Usage in the Filter
if(headers.get("Authorization") == null) { throw new NotLoggedInException("Not logged in"); }
 0
Author: Omnibyte,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-04-25 13:10:20